/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2015 ForgeRock AS.
 */
package org.forgerock.opendj.config;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * This class is used to map configuration elements to their LDAP schema names.
 * <p>
 * It is possible to augment the core LDAP profile with additional profile
 * mappings at run-time using instances of {@link Wrapper}. This is useful for
 * unit tests which need to add and remove mock components.
 */
public final class LDAPProfile {

    /**
     * LDAP profile wrappers can be used to provide temporary LDAP profile
     * information for components which do not have LDAP profile property files.
     * These components are typically "mock" components used in unit-tests.
     */
    public static abstract class Wrapper {

        /** Default constructor. */
        protected Wrapper() {
            // No implementation required.
        }

        /**
         * Get the name of the LDAP attribute associated with the specified
         * property definition.
         * <p>
         * The default implementation of this method is to return
         * <code>null</code>.
         *
         * @param d
         *            The managed object definition.
         * @param pd
         *            The property definition.
         * @return Returns the name of the LDAP attribute associated with the
         *         specified property definition, or <code>null</code> if the
         *         property definition is not handled by this LDAP profile
         *         wrapper.
         */
        public String getAttributeName(AbstractManagedObjectDefinition<?, ?> d, PropertyDefinition<?> pd) {
            return null;
        }

        /**
         * Gets the LDAP RDN attribute type for child entries of an instantiable
         * relation.
         * <p>
         * The default implementation of this method is to return
         * <code>null</code>.
         *
         * @param r
         *            The instantiable relation.
         * @return Returns the LDAP RDN attribute type for child entries of an
         *         instantiable relation, or <code>null</code> if the
         *         instantiable relation is not handled by this LDAP profile
         *         wrapper.
         */
        public String getRelationChildRDNType(InstantiableRelationDefinition<?, ?> r) {
            return null;
        }

        /**
         * Gets the LDAP RDN attribute type for child entries of an set
         * relation.
         * <p>
         * The default implementation of this method is to return
         * <code>null</code>.
         *
         * @param r
         *            The set relation.
         * @return Returns the LDAP RDN attribute type for child entries of an
         *         set relation, or <code>null</code> if the set relation is not
         *         handled by this LDAP profile wrapper.
         */
        public String getRelationChildRDNType(SetRelationDefinition<?, ?> r) {
            return null;
        }

        /**
         * Get the principle object class associated with the specified
         * definition.
         * <p>
         * The default implementation of this method is to return
         * <code>null</code>.
         *
         * @param d
         *            The managed object definition.
         * @return Returns the principle object class associated with the
         *         specified definition, or <code>null</code> if the managed
         *         object definition is not handled by this LDAP profile
         *         wrapper.
         */
        public String getObjectClass(AbstractManagedObjectDefinition<?, ?> d) {
            return null;
        }

        /**
         * Get an LDAP RDN sequence associatied with a relation.
         * <p>
         * The default implementation of this method is to return
         * <code>null</code>.
         *
         * @param r
         *            The relation.
         * @return Returns the LDAP RDN sequence associatied with a relation, or
         *         <code>null</code> if the relation is not handled by this LDAP
         *         profile wrapper.
         */
        public String getRelationRDNSequence(RelationDefinition<?, ?> r) {
            return null;
        }
    }

    /** The singleton instance. */
    private static final LDAPProfile INSTANCE = new LDAPProfile();

    /**
     * Get the global LDAP profile instance.
     *
     * @return Returns the global LDAP profile instance.
     */
    public static LDAPProfile getInstance() {
        return INSTANCE;
    }

    /** The list of profile wrappers. */
    private final LinkedList<Wrapper> profiles = new LinkedList<>();

    /** The LDAP profile property table. */
    private final ManagedObjectDefinitionResource resource = ManagedObjectDefinitionResource.createForProfile("ldap");

    /** Prevent construction. */
    private LDAPProfile() {
        // No implementation required.
    }

    /**
     * Get the name of the LDAP attribute associated with the specified property
     * definition.
     *
     * @param d
     *            The managed object definition.
     * @param pd
     *            The property definition.
     * @return Returns the name of the LDAP attribute associated with the
     *         specified property definition.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public String getAttributeName(AbstractManagedObjectDefinition<?, ?> d, PropertyDefinition<?> pd) {
        for (Wrapper profile : profiles) {
            String attributeName = profile.getAttributeName(d, pd);
            if (attributeName != null) {
                return attributeName;
            }
        }
        return resource.getString(d, "attribute." + pd.getName());
    }

    /**
     * Gets the LDAP RDN attribute type for child entries of an instantiable
     * relation.
     *
     * @param r
     *            The instantiable relation.
     * @return Returns the LDAP RDN attribute type for child entries of an
     *         instantiable relation.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public String getRelationChildRDNType(InstantiableRelationDefinition<?, ?> r) {
        if (r.getNamingPropertyDefinition() != null) {
            // Use the attribute associated with the naming property.
            return getAttributeName(r.getChildDefinition(), r.getNamingPropertyDefinition());
        } else {
            for (Wrapper profile : profiles) {
                String rdnType = profile.getRelationChildRDNType(r);
                if (rdnType != null) {
                    return rdnType;
                }
            }
            return resource.getString(r.getParentDefinition(), "naming-attribute." + r.getName());
        }
    }

    /**
     * Gets the LDAP object classes associated with an instantiable or set
     * relation branch. The branch is the parent entry of child managed objects.
     *
     * @param r
     *            The instantiable or set relation.
     * @return Returns the LDAP object classes associated with an instantiable
     *         or set relation branch.
     */
    public List<String> getRelationObjectClasses(RelationDefinition<?, ?> r) {
        return Arrays.asList(new String[] { "top", "ds-cfg-branch" });
    }

    /**
     * Gets the LDAP RDN attribute type for child entries of an set relation.
     *
     * @param r
     *            The set relation.
     * @return Returns the LDAP RDN attribute type for child entries of an set
     *         relation.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public String getRelationChildRDNType(SetRelationDefinition<?, ?> r) {
        for (Wrapper profile : profiles) {
            String rdnType = profile.getRelationChildRDNType(r);
            if (rdnType != null) {
                return rdnType;
            }
        }
        return resource.getString(r.getParentDefinition(), "naming-attribute." + r.getName());
    }

    /**
     * Get the principle object class associated with the specified definition.
     *
     * @param d
     *            The managed object definition.
     * @return Returns the principle object class associated with the specified
     *         definition.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public String getObjectClass(AbstractManagedObjectDefinition<?, ?> d) {
        if (d.isTop()) {
            return "top";
        }

        for (Wrapper profile : profiles) {
            String objectClass = profile.getObjectClass(d);
            if (objectClass != null) {
                return objectClass;
            }
        }
        return resource.getString(d, "objectclass");
    }

    /**
     * Get all the object classes associated with the specified definition.
     * <p>
     * The returned list is ordered such that the uppermost object classes
     * appear first (e.g. top).
     *
     * @param d
     *            The managed object definition.
     * @return Returns all the object classes associated with the specified
     *         definition.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public List<String> getObjectClasses(AbstractManagedObjectDefinition<?, ?> d) {
        LinkedList<String> objectClasses = new LinkedList<>();
        Set<String> s = new HashSet<>();

        // Add the object classes from the parent hierarchy.
        while (d != null) {
            String oc = getObjectClass(d);
            if (s.add(oc)) {
                objectClasses.addFirst(oc);
            }
            d = d.getParent();
        }

        if (!s.contains("top")) {
            objectClasses.addFirst("top");
        }

        return objectClasses;
    }

    /**
     * Get an LDAP RDN sequence associated with a relation.
     *
     * @param r
     *            The relation.
     * @return Returns the LDAP RDN sequence associated with a relation.
     * @throws MissingResourceException
     *             If the LDAP profile properties file associated with the
     *             provided managed object definition could not be loaded.
     */
    public String getRelationRDNSequence(RelationDefinition<?, ?> r) {
        for (Wrapper profile : profiles) {
            String rdnSequence = profile.getRelationRDNSequence(r);
            if (rdnSequence != null) {
                return rdnSequence;
            }
        }
        return resource.getString(r.getParentDefinition(), "rdn." + r.getName());
    }

    /**
     * Removes the last LDAP profile wrapper added using
     * {@link #pushWrapper(org.forgerock.opendj.config.LDAPProfile.Wrapper)}.
     *
     * @throws NoSuchElementException
     *             If there are no LDAP profile wrappers.
     */
    public void popWrapper() {
        profiles.removeFirst();
    }

    /**
     * Decorates the core LDAP profile with the provided LDAP profile wrapper.
     * All profile requests will be directed to the provided wrapper before
     * being forwarded onto the core profile if the request could not be
     * satisfied.
     *
     * @param wrapper
     *            The LDAP profile wrapper.
     */
    public void pushWrapper(Wrapper wrapper) {
        profiles.addFirst(wrapper);
    }
}
